<html>
  <head>
    <title>webgl-bench -- WebGL performance info</title>

<script src="static/util.js"></script>
<script src="static/plot.js"></script>

<!-- Shader code ------------------------------ -->

<script id="vertexshader-t1" type="x-shader/x-vertex">
precision highp float;

uniform mat4 MVP;

attribute vec3 position;
attribute vec2 t0_uv;

varying vec2 uv;

void main() {
  gl_Position = MVP * vec4(position, 1.0);
  uv = t0_uv;
}
</script>

<script id="fragmentshader-t1" type="x-shader/x-fragment">
precision highp float;

uniform sampler2D t0_sampler;
varying vec2 uv;

void main() {
  gl_FragColor = texture2D(t0_sampler, uv);
}
</script>

<!-- Javascript code ------------------------------ -->

    <script src="static/glmath.js"></script>

    <script>
// Export the glmath symbols.
for (var k in glmath) {
  window[k] = glmath[k];
}

// Float32Array compatibility.
if ('Float32Array' in window) {
  // Leave Float32Array as-is.
} else if ('FloatArray' in window) {
  window.Float32Array = FloatArray;
} else if ('WebGLFloatArray' in window) {
  window.Float32Array = WebGLFloatArray;
} else {
  // Trouble.
  throw "No Float32Array implementation found.";
}

// Uint16Array compatibility.
if ('Uint16Array' in window) {
  // Leave Uint16Array as-is.
} else if ('WebGLUnsignedShortArray' in window) {
  window.Uint16Array = WebGLUnsignedShortArray;
} else {
  throw "No Uint16Array implementation found.";
}

var gl;
var program;
var modelMatrixLocation, modelMatrixGl;
var viewMatrixLocation, viewMatrixGl, viewMatrixJs, viewPosJs;
var projectionMatrixLocation, projectionMatrixGl;
var projectionMatrix;
var mvpLocation;
var position_location;
var t0_uv_location;

var identityMatrix;

var fovy = Math.PI / 3;  // radians
var width, height;
var z_near = 0.1;
var z_far = 20;

var draw_repeat_x = 1;

// Stats.
var tri_count = 0;
var draw_count = 0;

var initialCameraOrient = makeOrientationAffine(
    {x: 5, y: 0, z: 0},  // pos
    {x: -1, y: 0, z: 0},  // dir
    {x: 0, y: 1, z: 0} // up
                                                );
var cameraOrient = {};
copyAffineMatrix(cameraOrient, initialCameraOrient);

var world = [];
var uniform_grid = null;

var enable_layer = [true, true, true, true, true, true];

function dampedDrive(x0, x1, tc, dt) {
  var c0 = Math.exp(-dt / tc);
  return x0 * c0 + x1 * (1 - c0);
}

function vectorDampedDrive(v0, v1, tc, dt) {
  var c0 = Math.exp(-dt / tc);
  return vectorAdd(vectorScale(v0, c0), vectorScale(v1, 1 - c0));
}

function lerper(x0, x1, t) {
  return (t - x0) / (x1 - x0);
}

function make_grid_triangles(dim) {
  // "Slab" is a strip of squares; idea is to get a reasonable vertex
  // cache hit-rate.
  //
  // TODO: Investigate alternative vert orderings and slab heights.
  var SLAB_HEIGHT = 8;
  var inds = new Uint16Array((dim - 1) * (dim - 1) * 6);
  var next_i = 0;
  for (var slab_j = 0; slab_j < dim - 1; slab_j += SLAB_HEIGHT) {
    for (var i = 0; i < dim - 1; i++) {
      var end_j = Math.min(dim - 1, slab_j + SLAB_HEIGHT);
      for (var j = slab_j; j < end_j; j++) {
        var vi = j * dim + i;
        //  *--*
        //  |\ |
        //  | \|
        //  *--*
        inds[next_i++] = vi;
        inds[next_i++] = vi + dim + 1;
        inds[next_i++] = vi + dim;
        inds[next_i++] = vi;
        inds[next_i++] = vi + 1;
        inds[next_i++] = vi + dim + 1;
      }
    }
  }
  return inds;
}

function make_grid_triangle_strip(dim) {
  // "Slab" is a strip of squares; idea is to get a reasonable vertex
  // cache hit-rate.
  //
  // TODO: Investigate alternative vert orderings and slab heights.
  var SLAB_HEIGHT = 8;
  var inds = [];
  for (var slab_j = 0; slab_j < dim - 1; slab_j += SLAB_HEIGHT) {
    for (var i = 0; i < dim - 1; i++) {
      var end_j = Math.min(dim - 1, slab_j + SLAB_HEIGHT);
      var vi = slab_j * dim + i;
      if (inds.length) {
        // Start the strip with a degenerate if we're connecting to a
        // previous strip.
        inds.push(vi + 1);
      }
      inds.push(vi + 1);
      inds.push(vi);
      for (var j = slab_j; j < end_j; j++) {
        //  *--*
        //  |\ | ...
        //  | \|
        //  *--*
        //  |\ |
        //  | \|
        //  *--*
        //  |\ |
        //  | \|
        //  *--*
        //  |\ |
        //  | \|
        //  *--*
        //   .
        //   .
        //   .
        inds.push(vi + dim + 1);
        inds.push(vi + dim);
        vi += dim;
      }
      // End the strip with a degenerate tri in order to start the
      // next strip.
      inds.push(inds[inds.length - 1]);
    }
  }
  var inds_array = new Uint16Array(inds);
  return inds_array;
}

function get_texture(url) {
  check_gl_error('get_texture 1');
  var texture = gl.createTexture();
  check_gl_error('get_texture 2');
  var image = new Image();
  image.src = url;
  image.onload = function() {
    check_gl_error('get_texture 3');
    gl.bindTexture(gl.TEXTURE_2D, texture);
    check_gl_error('get_texture 3.1');
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
    check_gl_error('get_texture 3.2');
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    check_gl_error('get_texture 3.3');
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
    check_gl_error('get_texture 3.4');
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    check_gl_error('get_texture 3.5');
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    check_gl_error('get_texture 3.6');
    gl.generateMipmap(gl.TEXTURE_2D);
    check_gl_error('get_texture 3.7');
    gl.bindTexture(gl.TEXTURE_2D, null);
    check_gl_error('get_texture 4');
  };
  return texture;
}

function init_model(model) {
  check_gl_error('init_model pre');
  // Just a square.
  var verts = new Float32Array([0, 0, 0, 0, 1,   // x, y, z, u, v
                                2, 0, 0, 1, 1,
                                0, 2, 0, 0, 0,
                                2, 2, 0, 1, 0]);
  var verts_buf = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, verts_buf);
  gl.bufferData(gl.ARRAY_BUFFER, verts, gl.STATIC_DRAW);

  check_gl_error('init_model 1');

  var inds_array = new Uint16Array([0, 1, 2, 3]);
  var index_type = gl.TRIANGLE_STRIP;
  var inds_buf = gl.createBuffer();
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, inds_buf);
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, inds_array, gl.STATIC_DRAW);
  var index_count = inds_array.length;

  check_gl_error('init_model 2');

  // Fetch a texture.
  var texture = get_texture('static/logo.png');

  var model_mat = makeOrientationAffine({x:0,y:-1,z:1}, {x:0,y:0,z:-1}, {x:0,y:1,z:0});
  var model_mat_gl = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];
  affineMatrixToGlMatrix(model_mat, model_mat_gl);

  model.matrix_gl = model_mat_gl;
  model.vertices = verts_buf;
  model.indices = inds_buf;
  model.index_count = index_count;
  model.index_type = index_type;
  // gl.LINES -- poor-person's wireframe
  model.triangle_count = 2;
  model.texture = texture;

  check_gl_error('init_model post');
}

function shutdown_model(model) {
  // Release the pieces.
  if (model.texture) {
    gl.deleteTexture(model.texture);
  }
  gl.deleteBuffer(model.vertices);
  gl.deleteBuffer(model.indices);
  check_gl_error('shutdown_model');
}

function shutdown_world(world) {
  for (var i = 0; i < world.length; i++) {
    shutdown_model(world[i]);
  }
  world = [];
}

function draw_model(model) {
  if (!model.texture) {
    // No texture yet.  Don't draw.
    return;
  }
  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, model.texture);

  gl.bindBuffer(gl.ARRAY_BUFFER, model.vertices);
  gl.vertexAttribPointer(position_location, 3, gl.FLOAT, false, 4 * 5, 0);
  gl.vertexAttribPointer(t0_uv_location, 2, gl.FLOAT, false, 4 * 5, 4 * 3);
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, model.indices);
  gl.drawElements(model.index_type, model.index_count,
                  gl.UNSIGNED_SHORT, 0);
  tri_count += model.triangle_count;
  draw_count += 1;
}

function draw_world(world) {
  viewMatrixJs = makeViewFromOrientation(cameraOrient);
  affineMatrixToGlMatrix(viewMatrixJs, viewMatrixGl);

  var vp = myMatrixMultiply(projectionMatrix, viewMatrixGl);

  for (var i = 0; i < world.length; i++) {
    var model = world[i];
    var mvp = myMatrixMultiply(vp, model.matrix_gl);
    gl.uniformMatrix4fv(mvpLocation, false, mvp);
    draw_model(model);
  }
  check_gl_error('draw_world');
}

function get_context(canvas) {
  var gl;
  try { gl = canvas.getContext("webgl"); } catch(e) {}
  if (!gl) try { gl = canvas.getContext("webkit-3d"); } catch(e) {}
  if (!gl) try { gl = canvas.getContext("moz-webgl"); } catch(e) {}
  if (!gl) try { gl = canvas.getContext("experimental-webgl"); } catch(e) {}
  if (gl) {
    check_gl_error('get_context');
  }
  return gl;
}

function check_gl_error(tag) {
  if (!gl) { return; }
  var error = gl.getError();
  if (error != gl.NO_ERROR) {
    var str = 'GL Error: ' + error + ' (' + tag + ')';
    if (console) {
      console.log(str);
    }
    debugger;
    throw str;
  }
}

function load_shader(id, type) {
  var shader_el = el(id);
  var shader = gl.createShader(type);
  if (!shader) return null;
  gl.shaderSource(shader, shader_el.text);
  gl.compileShader(shader);
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    if (console) {
      console.log("Error compiling shader: " +
		  gl.getShaderInfoLog(shader));
    }
    return null;
  }
  check_gl_error('load_shader');
  return shader;
}

// Return a new WebGLFloatArray w/ the appropriate values.
//
// fovy in radians
// aspect ratio is x/y
function myGluPerspective(fovy, aspect, z_near, z_far) {
  var f = 1 / Math.tan(fovy / 2);
  return new Float32Array([
      f / aspect, 0, 0, 0,
      0, f, 0, 0,
      0, 0, (z_far + z_near) / (z_near - z_far), -1,
      0, 0, 2 * z_far * z_near / (z_near - z_far), 0
                               ]);
}

function myMatrixMultiply(ma, mb) {
  var result = new Float32Array(16);
  for (var i = 0; i < 4; i++) {
    for (var j = 0; j < 4; j++) {
      var val = 0;
      for (var k = 0; k < 4; k++) {
	val += ma[j + k * 4] * mb[k + i * 4];
      }
      result[j + i * 4] = val;
    }
  }
  return result;
}

function reshape(canvas, w, h) {
  width = w;
  height = h;

  gl.viewport(0, 0, w, h);
}

function add_script_element(url, type) {
  var head = document.getElementsByTagName('head')[0];
  var script = document.createElement('script');
  script.type = type;
  script.src = url;
  script.id = url;
  head.appendChild(script);
}

function fetch_script(url, type) {
  if (el(url)) {
    // Already fetched something under this name.
    return;
  }
  var head = document.getElementsByTagName('head')[0];
  var script = document.createElement('script');
  script.type = type;

  // Synchronous XHR to fill script text...  TODO: fix this?
  var xhr = new XMLHttpRequest();
  xhr.open('GET', url, false);
  xhr.send(null);
  script.text = xhr.responseText;

  script.id = url;
  head.appendChild(script);
}

function fetch_load_and_link_program(vshader_name, fshader_name) {
  fetch_script(vshader_name, 'x-shader/x-vertex');
  fetch_script(fshader_name, 'x-shader/x-fragment');
  return load_and_link_program(vshader_name, fshader_name);
}

function load_and_link_program(vshader_id, fshader_id) {
  var vshader = load_shader(vshader_id, gl.VERTEX_SHADER);
  if (!vshader) return null;

  var fshader = load_shader(fshader_id, gl.FRAGMENT_SHADER);
  if (!fshader) return null;

  var program = gl.createProgram();
  if (!program) return null;

  gl.attachShader(program, vshader);
  gl.attachShader(program, fshader);
  gl.linkProgram(program);
  check_gl_error('load_and_link_program ' + vshader_id + ', ' + fshader_id);

  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    if (console) {
      console.log('Error in program linking:' + gl.getProgramInfoLog(program));
    }
    return null;
  }
  return program;
}

function init_gl() {
  try {
    var canvas = el('canvas');
    gl = get_context(canvas);
    if (!gl) {
      return null;
    }

    program = load_and_link_program('vertexshader-t1', 'fragmentshader-t1');

    gl.useProgram(program);

    check_gl_error('init_gl 2');

    modelMatrixLocation = gl.getUniformLocation(program, 'modelMatrix');
    viewMatrixLocation = gl.getUniformLocation(program, 'viewMatrix');
    projectionMatrixLocation = gl.getUniformLocation(program, 'projectionMatrix');
    mvpLocation = gl.getUniformLocation(program, 'MVP');
    modelMatrixGl = new Float32Array([
                                         1, 0, 0, 0,
                                         0, 1, 0, 0,
                                         0, 0, 1, 0,
                                         0, 0, 0, 1,
                                      ]);
    viewMatrixGl = new Float32Array([
                                        1, 0, 0, 0,
                                        0, 1, 0, 0,
                                        0, 0, 1, 0,
                                        0, 0, 0, 1,
                                     ]);
    identityMatrix = new Float32Array([
                                          1, 0, 0, 0,
                                          0, 1, 0, 0,
                                          0, 0, 1, 0,
                                          0, 0, 0, 1,
                                       ]);

    check_gl_error('init_gl 3');

    gl.uniform1i(gl.getUniformLocation(program, 't0_sampler'), 0);

    check_gl_error('init_gl 4');

    reshape(canvas, canvas.width, canvas.height);

    position_location = gl.getAttribLocation(program, 'position');
    gl.enableVertexAttribArray(position_location);
    t0_uv_location = gl.getAttribLocation(program, 't0_uv');
    gl.enableVertexAttribArray(t0_uv_location);

    gl.clearColor(0, 0, 0, 1);
    gl.clearDepth(1.0);

    check_gl_error('init_gl post');
    return gl;
  } catch (err) {
    gl = null;
    if (console) {
      console.log(err);
    }
    return null;
  }
}


function draw() {
  projectionMatrix = myGluPerspective(fovy, width / height, z_near, z_far);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  gl.enable(gl.DEPTH_TEST);
  gl.depthFunc(gl.LEQUAL);
  //gl.enable(gl.CULL_FACE);
  //gl.cullFace(gl.BACK);
  gl.useProgram(program);

  draw_world(world);

  check_gl_error('draw');
}

var start, last_timestamp = 0;
var metrics;
var frame_number = 0;
var paused = true;
var refresh_timer = -1;

var canvas_elem;
var canvas_x, canvas_y;
var zoom_in_pressed = false;
var zoom_out_pressed = false;
var up_pressed = false;
var down_pressed = false;
var right_pressed = false;
var left_pressed = false;
var shift_pressed = false;
var ctrl_pressed = false;
var alt_pressed = false;
var mouse_is_down = false;
var right_mouse_is_down = false;
var last_right_mouse_down = 0;
var mouse_x = 0;
var mouse_y = 0;
var mouse_down_x = 0;
var mouse_down_y = 0;
var mouse_grab_point = null;
var mouse_double_clicked = false;
var right_mouse_double_clicked = false;
var globe_omega = { x: 0, y: 0, z: 0 };

var tangent_dir = { x: 0, y: 1, z: 0 };
var anchor = { x: 0, y: 0, z: 1 };
var target_anchor = { x: 1, y: 0, z: 0 };
var pitch = -Math.PI / 2;
var target_pitch = -Math.PI / 2;
var initialTargetDist = 1.3;
var target_dist = initialTargetDist;
var dist = 5;

function keydown(event) {
  set_paused(false);
  if (event.keyCode == 33) {  // PgUp
    zoom_in_pressed = true;
  } else if (event.keyCode == 34) {  // PgDn
    zoom_out_pressed = true;
  } else if (event.keyCode == 38) {  // Up
    up_pressed = true;
  } else if (event.keyCode == 40) {  // Down
    down_pressed = true;
  } else if (event.keyCode == 39) {  // Right
    right_pressed = true;
  } else if (event.keyCode == 37) {  // Left
    left_pressed = true;
  } else if (event.keyCode == 16) {  // Shift
    shift_pressed = true;
  } else if (event.keyCode == 17) {  // Ctrl
    ctrl_pressed = true;
  } else if (event.keyCode == 18) {  // Alt
    alt_pressed = true;
  } else if (event.keyCode >= 48 && event.keyCode < 58) {  // number keys
    var num = event.keyCode - 48;
    // Toggle visibility.
    if (num < 6) {
      enable_layer[num] = !enable_layer[num];
    }
  }
}

function keyup(event) {
  if (event.keyCode == 33) {  // PgUp
    zoom_in_pressed = false;
  } else if (event.keyCode == 34) {  // PgDn
    zoom_out_pressed = false;
  } else if (event.keyCode == 38) {  // Up
    up_pressed = false;
  } else if (event.keyCode == 40) {  // Down
    down_pressed = false;
  } else if (event.keyCode == 39) {  // Right
    right_pressed = false;
  } else if (event.keyCode == 37) {  // Left
    left_pressed = false;
  } else if (event.keyCode == 16) {  // Shift
    shift_pressed = false;
  } else if (event.keyCode == 17) {  // Ctrl
    ctrl_pressed = false;
  } else if (event.keyCode == 18) {  // Alt
    alt_pressed = false;
  }
}

function rememberMousePos(e) {
  var width = canvas_elem.width;
  var height = canvas_elem.height;
  mouse_x = ((e.pageX - canvas_x) - width / 2) / (height / 2);
  mouse_y = -(((e.pageY - canvas_y) - height / 2) / (height / 2));
}

function mousedown(e) {
  mouse_is_down = (e.which == 1);
  right_mouse_is_down = (e.which == 3);
  if (right_mouse_is_down) {
    var timestamp = new Date().getTime();
    if (timestamp - last_right_mouse_down < 250) {
      right_mouse_double_clicked = true;
    }
    last_right_mouse_down = timestamp;
  }
  rememberMousePos(e);
  mouse_down_x = mouse_x;
  mouse_down_y = mouse_y;
  e.preventDefault();
}

function mouseclick(e) {
  e.stopPropagation();
  e.preventDefault();
  return false;
}

function mousedblclick(e) {
  if (ctrl_pressed) {
    right_mouse_double_clicked = true;
  } else {
    mouse_double_clicked = true;
  }
  rememberMousePos(e);
  set_paused(false);
//   e.preventDefault();
}

function mousemove(e) {
  rememberMousePos(e);
  if (mouse_is_down || right_mouse_is_down) {
    set_paused(false);
  }
}

function mouseup(e) {
  if (e.which == 1) {
    mouse_is_down = false;
  }
  if (e.which == 3) {
    right_mouse_is_down = false;
    set_paused(false);
  }
}

// Return the first exterior hit or closest point between the unit
// sphere and the ray starting at p and going in the r direction.
function rayVsUnitSphereClosestPoint(p, r) {
  var p_len2 = dotProduct(p, p);
  if (p_len2 < 1) {
    // Ray is inside sphere, no exterior hit.
    return null;
  }

  var along_ray = -dotProduct(p, r);
  if (along_ray < 0) {
    // Behind ray start-point.
    return null;
  }

  var perp = vectorAdd(p, vectorScale(r, along_ray));
  var perp_len2 = dotProduct(perp, perp);
  if (perp_len2 >= 0.999999) {
    // Return the closest point.
    return vectorNormalize(perp);
  }

  // Compute intersection.
  var e = Math.sqrt(1 - dotProduct(perp, perp));
  var hit = vectorAdd(p, vectorScale(r, (along_ray - e)));
  return vectorNormalize(hit);
}

function screenToSpherePt(x, y) {
  var p = matrixColumn(cameraOrient, 3);
  // camera dir
  var r = matrixColumn(cameraOrient, 0);
  var up = matrixColumn(cameraOrient, 1);
  var right = matrixColumn(cameraOrient, 2);
  var tan_half = Math.tan(fovy / 2);
  r = vectorAdd(r, vectorScale(right, x * tan_half));
  r = vectorAdd(r, vectorScale(up, y * tan_half));
  r = vectorNormalize(r);

  return rayVsUnitSphereClosestPoint(p, r);
}

function rotateGlobe(scaled_axis) {
  var angle = -Math.asin(Math.sqrt(dotProduct(scaled_axis, scaled_axis)));
  var axis = vectorNormalize(scaled_axis);
  var mat = makeRotateAxisAngle(axis, angle);
  anchor = applyRotation(mat, anchor);
  target_anchor = vectorNormalize(applyRotation(mat, target_anchor));
  tangent_dir = applyRotation(mat, tangent_dir);
}

function update(dt) {
  // Navigation.
  var MIN_DIST = 5e-6;
  var MAX_DIST = 10;
  var zoom_speed = lerp(1e-5, 10, lerper(MIN_DIST, MAX_DIST, dist));
  var keep_running = false;

  if (zoom_in_pressed) {
    target_dist -= zoom_speed * dt;
  }
  if (zoom_out_pressed) {
    target_dist += zoom_speed * dt;
  }

  if (mouse_double_clicked) {
    mouse_double_clicked = false;
    target_anchor = screenToSpherePt(mouse_x, mouse_y);
    target_dist /= 2;
  }
  if (right_mouse_double_clicked && !right_mouse_is_down) {
    right_mouse_double_clicked = false;
    target_anchor = screenToSpherePt(mouse_x, mouse_y);
    target_dist *= 2;
  }

  if (shift_pressed) {
    // Pitch control.
    if (up_pressed) {
      target_pitch -= 1 * dt;
      if (target_pitch < -Math.PI / 2) {
        target_pitch = -Math.PI / 2;
      }
    }
    if (down_pressed) {
      target_pitch += 1 * dt;
      if (target_pitch > 0) {
        target_pitch = 0;
      }
    }
    // Roll control.
    if (left_pressed) {
      var mat = makeRotateAxisAngle(anchor, 1 * dt);
      tangent_dir = applyRotation(mat, tangent_dir);
    }
    if (right_pressed) {
      var mat = makeRotateAxisAngle(anchor, -1 * dt);
      tangent_dir = applyRotation(mat, tangent_dir);
    }
  } else {
    // Move the anchor.
    if (left_pressed) {
      rotateGlobe(vectorScale(tangent_dir, 1 * dt * zoom_speed));
    }
    if (right_pressed) {
      rotateGlobe(vectorScale(tangent_dir, -1 * dt * zoom_speed));
    }
    if (up_pressed || down_pressed) {
      var right_axis = crossProduct(tangent_dir, anchor);
      if (up_pressed) {
        rotateGlobe(vectorScale(right_axis, 1 * dt * zoom_speed));
      }
      if (down_pressed) {
        rotateGlobe(vectorScale(right_axis, -1 * dt * zoom_speed));
      }
    }
  }

  if (mouse_is_down) {
    var new_grab_point = screenToSpherePt(mouse_x, mouse_y);
    if (mouse_grab_point == null && new_grab_point != null) {
      mouse_grab_point = new_grab_point;
    }
    if (mouse_grab_point && new_grab_point) {
      // Rotate the earth, to map old grab point onto new grab point.
      var axis = crossProduct(mouse_grab_point, new_grab_point);
      axis = vectorScale(axis, 0.85);
      rotateGlobe(axis);

      globe_omega = vectorScale(axis, 1000 / dt);
    }
    keep_running = true;
  } else {
     // Momentum spin.
    mouse_grab_point = null;

    globe_omega = vectorDampedDrive(globe_omega, {x:0, y:0, z:0}, 0.95, dt);
    var axis = vectorScale(globe_omega, dt / 1000);
    rotateGlobe(axis);
  }

  if (right_mouse_is_down) {
    // Zoom in/out.
    var zoom_factor = 2 * (mouse_down_y - mouse_y);
    target_dist += zoom_factor * dt * zoom_speed;

    // Rotate.
    var angle_factor = 2 * (mouse_down_x - mouse_x);
    var mat = makeRotateAxisAngle(anchor, angle_factor * dt);
    tangent_dir = applyRotation(mat, tangent_dir);

    keep_running = true;
  }

  target_dist = clamp(target_dist, MIN_DIST, MAX_DIST);

  // Make cameraOrient based on params.
  dist = dampedDrive(dist, target_dist, 0.10, dt);
  pitch = dampedDrive(pitch, target_pitch, 0.10, dt);
  anchor = vectorNormalize(vectorDampedDrive(anchor, target_anchor, 0.10, dt));
  // Flatten the tangent to keep it tangent.
  tangent_dir = vectorNormalize(
      vectorSub(tangent_dir,
                vectorScale(anchor, dotProduct(anchor, tangent_dir))));

  var pos = vectorAdd(vectorScale(anchor, 1 - dist * Math.sin(pitch)),
                      vectorScale(tangent_dir, - dist * Math.cos(pitch)));
  var dir = vectorNormalize(vectorSub(anchor, pos));
  var right = crossProduct(tangent_dir, anchor);
  var up = crossProduct(right, dir);
  cameraOrient = makeOrientationAffine(pos, dir, up);

  if (left_pressed || right_pressed || up_pressed || down_pressed ||
      zoom_in_pressed || zoom_out_pressed ||
      Math.abs(dist - target_dist) > 1e-6 ||
      Math.abs(pitch - target_pitch) > 1e-5 ||
      vectorLength(vectorSub(anchor, target_anchor)) > 1e-6 ||
      dotProduct(globe_omega, globe_omega) > 0.000001) {
    keep_running = true;
  }

  if (!keep_running) {
    globe_omega = {x: 0, y: 0, z: 0};
    set_paused(true);
  }

  normalizeMatrixRotation(cameraOrient);

  // Adjust z_near to pull it in close when camera is near the ground
  // (to avoid near-clip); push it out when camera is far away (to
  // avoid z-fighting).
  var normalized_altitude = vectorLength(matrixColumn(cameraOrient, 3));
  z_near = lerp(6e-7, 1e-1, Math.log(normalized_altitude) / Math.log(5));

  // Update our cached values of the location of the canvas on the
  // page (we use this for interpreting mouse input).
  canvas_x = 0;
  canvas_y = 0;
  var elem = canvas_elem;
  while (elem) {
    canvas_x += elem.offsetLeft;
    canvas_y += elem.offsetTop;
    elem = elem.offsetParent;
  }
}

function reset_view(pos, dir, up) {
  cameraOrient = makeOrientationAffine(pos, dir, up);
  globe_omega = {x: 0, y: 0, z: 0};
}

function init_world() {
  check_gl_error('init_world pre');
  world = [];
  var model = {};
  init_model(model);
  world.push(model);
  check_gl_error('init_world post');
}

var benchmarks = [];

function add_benchmark(name) {
  add_script_element('static/' + name + '.js', 'text/javascript');
  benchmarks.push(name);
}

var report;
var current_benchmark_name = '';
var next_benchmark = 0;
var benchmark_subtick = 0;
var benchmark_continuation_callback = null;

function add_metric(name, value) {
  report.metric[name] = Number(value);
}

function continue_benchmark(callback) {
  benchmark_continuation_callback = callback;
}

function show_benchmark_progress() {
  el('benchmark_status').innerHTML = "WORKING " + (next_benchmark) + "." +
                                     benchmark_subtick + "/" +
                                     benchmarks.length + " " +
                                     current_benchmark_name;
}

function tick_benchmark() {
  if (benchmark_continuation_callback) {
    benchmark_subtick++;
    show_benchmark_progress();
    var cb = benchmark_continuation_callback;
    benchmark_continuation_callback = null;
    try {
      cb();
      benchmark_subtick++;
    } catch (e) {
      if (console) {
        console.log("benchmark continuation threw exception, " + e);
      }
      benchmark_continuation_callback = null;
    }

    // Yield to browser before continuing.
    setTimeout(tick_benchmark, 0);
    return;
  }

  if (next_benchmark >= benchmarks.length) {
    // Done.  Do reports.
    end_benchmark();
    return;
  }

  // Call the next benchmark entry point.
  try {
    benchmark_subtick = 0;
    current_benchmark_name = benchmarks[next_benchmark];
    var benchmark_constructor = window[current_benchmark_name];
    var current_benchmark = new benchmark_constructor();
    current_benchmark.tick();
  } catch (e) {
    if (console) {
      console.log("benchmark threw exception, " + e);
    }
    benchmark_continuation_callback = null;
  }
  next_benchmark++;
  show_benchmark_progress();

  setTimeout(tick_benchmark, 100);
}

function start_benchmark() {
  set_paused(true);
  el('start_benchmark').disabled = true;
  el('benchmark_status').innerHTML = 'STARTING';
  el('upload_status').innerHTML = '';
  report = {
    tag: "self",
    reporter: {
      userAgent: navigator.userAgent,
      GL_VERSION: get_param(gl.VERSION),
      GL_VENDOR: get_param(gl.VENDOR),
      GL_RENDERER: get_param(gl.RENDERER),
      GL_SHADING_LANGUAGE_VERSION: get_param(gl.SHADING_LANGUAGE_VERSION)
    },
    metric: {}
  };

  // Run the registered benchmarks.
  next_benchmark = 0;
  tick_benchmark();
}

function pad(x, width) {
  var text = '' + x;
  while (text.length < width) {
    text = ' ' + text;
  }
  return text;
}

function padright(x, width) {
  var text = '' + x;
  while (text.length < width) {
    text += ' ';
  }
  return text;
}

function get_param(x) {
  if (gl.getString) {
    return gl.getString(x);
  } else {
    // TODO: legacy -- I think this can go away now?
    return gl.getParameter(x);
  }
}

function end_benchmark() {
  el('benchmark_status').innerHTML = 'DONE';

  var text = '';
  for (var r in report.reporter) {
    text += r + ' = ' + report.reporter[r] + '\n';
  }
  text += '+------------------------------------------+--------------+\n';
  text += '| metric                                   | value        |\n';
  text += '+------------------------------------------+--------------+\n';
  for (var m in report.metric) {
    text += '| ' + padright(m, 40) + ' | ' + pad(report.metric[m], 12) + ' |\n';
  }
  text += '+------------------------------------------+--------------+\n';

  el('report').value = text;

  if (!el('auto_upload').checked) {
    // Add a button to report the value.
    el('upload_results').addEventListener('click', upload_report, false);
    el('upload_results').disabled = false;
  } else {
    upload_report();
    el('upload_results').disabled = true;
  }

  update_plots('statsl', el('sb_category_l').value);
  update_plots('statsr', el('sb_category_r').value);

  el('start_benchmark').disabled = false;

  reinit_world();
  set_paused(false);
}

function upload_report() {
  el('upload_status').innerHTML = 'upload: preparing';
  var xhr = new XMLHttpRequest();
  xhr.onreadystatechange = function() {
    if (this.readyState == 4) {
      if (this.status == 200) {
        el('upload_status').innerHTML = 'upload: OK';
	el('upload_results').disabled = true;
	el('auto_upload').checked = false;
      } else {
        el('upload_status').innerHTML = 'upload: failure, status = ' + this.status;
        el('upload_results').disabled = false;
      }
    }
  };
  xhr.open("POST", "/upload");
  xhr.setRequestHeader('Content-Type', 'text/plain;charset=UTF-8');
  xhr.send(JSON.stringify(report));
  el('upload_status').innerHTML = 'upload: posting';

  //console.log(JSON.stringify(report));//xxxxxxx
}

function update_plots(elem_id, category) {
  var elem = el(elem_id);
  var tag_categories = {
    'default': ['all'],
    'browser': ['all', 'browser_firefox', 'browser_chrome',
                'browser_safari', 'browser_opera'],
    'os': ['all', 'os_windows', 'os_macintosh', 'os_linux'],
    'gpu': ['all', 'gpu_nvidia', 'gpu_ati', 'gpu_angle', 'gpu_intel', 'gpu_other'],
    'mobile': ['os_mobile', 'os_ios', 'os_android']
  };
  if (tag_categories[category] == undefined) {
    category = 'default';
  }
  var tags = tag_categories[category];

  update_stats_plots(elem, report, tags);
}

function update_stats_plots(elem, self_report, opt_tags) {
  var tags = opt_tags || ['all', 'browser_firefox', 'browser_chrome',
                          'browser_safari', 'browser_opera'];
  var xhr = new XMLHttpRequest();
  xhr.onreadystatechange = function() {
    if (this.readyState == 4) {
      if (this.status == 200) {
        try {
          update_stats_with_data(elem, JSON.parse(this.responseText), self_report);
        }
        catch (e) {
          log('update_stats_with_data failed, e = ', e);
        }
      } else {
        log('update_stats fetch failed, status = ' + this.status);
      }
    }
  }
  xhr.open('GET', '/get_data?tags=' + tags.join(','));
  xhr.send();
}

function update_stats_with_data(elem, datasets, self_report) {
  // For a tiny graph:
  //   var opts = {
  //     width: 200,
  //     row_pitch: 5
  //   };
  var opts = { width: 600,
               row_pitch: 30,
               label_width: 300,
               text_height: 14,
               small_text_height: 10,
               label_axes: true };
  var small_opts = { width: 600,
                     row_pitch: 5,
                     label_width: 300,
                     text_height: 14,
                     small_text_height: 0,
                     label_axes: true };

  var s = elem;
  s.innerHTML = '';
  // Show the given data.  If we have a locally-measured dataset, put
  // it in with the remote stats.
  var ds = array_copy(datasets);
  if (self_report) {
    ds.push(self_report);
  }
  var p = new Plot(ds);
  p.appendPlot(s, opts, small_opts);
}

function reinit_world() {
  init_world();
}

function start() {
  if (document.cookie && document.cookie.indexOf('dedupe_hash=') != -1) {
    el('auto_upload').checked = false;
  } else {
    el('auto_upload').checked = true;
  }

  update_plots('statsl', 'gpu');
  el('sb_category_l').value = 'gpu';
  update_plots('statsr', 'browser');
  el('sb_category_r').value = 'browser';

  init_gl();
  if (!gl) {
    el('init_failure').style.display = '';
    return;
  }

  reinit_world();

  start = new Date().getTime();
  var met = {timestamp: start, draws: 0, tris: 0};
  metrics = [met, met, met, met, met, met, met, met, met, met, met, met];
  frame_number = 0;

  set_paused(false);

  canvas_elem = el('canvas');
  canvas_elem.addEventListener('mousedown', mousedown, false);
  canvas_elem.addEventListener('click', mouseclick, false);
  canvas_elem.addEventListener('dblclick', mousedblclick, false);
  document.addEventListener('mousemove', mousemove, false);
  document.addEventListener('mouseup', mouseup, false);
  document.body.addEventListener('keydown', keydown, false);
  document.body.addEventListener('keyup', keyup, false);

  // Disable the context menu over the canvas so we can process right-clicks.
  canvas_elem.addEventListener('contextmenu', function(e) {
      e.preventDefault(); }, false);

  el('start_benchmark').addEventListener('click', start_benchmark, false);
}

function refresh_function() {
  var timestamp = new Date().getTime();
  var dt = (timestamp - last_timestamp) / 1000.0;
  last_timestamp = timestamp;

  tri_count = 0;
  draw_count = 0;

  update(dt);
  for (var i = 0; i < draw_repeat_x; i++) {
    draw();
  }

  var met = {timestamp: timestamp, draws: draw_count, tris: tri_count};
  metrics[frame_number % metrics.length] = met;
  var frame_ct = 0;
  var frame_ms = 0;
  var total_draws = 0;
  var total_tris = 0;
  for (var i = metrics.length; i > 1; i--) {
    var met0 = metrics[(frame_number + i) % metrics.length];
    var met_minus_1 = metrics[(frame_number + i - 1) % metrics.length];
    if (met0 && met_minus_1) {
      var t0 = met0.timestamp;
      var t_minus_1 = met_minus_1.timestamp;
      frame_ms += t0 - t_minus_1;
      frame_ct++;
      total_draws += met0.draws;
      total_tris += met0.tris;
    }
  }
  frame_number++;
}

function set_paused(new_paused) {
  if (paused == new_paused) {
    return;
  }
  paused = new_paused;

  if (refresh_timer != -1) {
    clearInterval(refresh_timer);
    refresh_timer = -1;
  }

  if (!paused) {
    refresh_timer = setInterval(refresh_function, 1 /*16*/);
    last_timestamp = new Date().getTime();
  } else {
    metrics[frame_number % metrics.length] = null;
    frame_number++;
  }
}

function resize(w, h) {
  var canvas = document.getElementById('canvas');
  var gl = get_context(canvas);

  canvas.setAttribute('width', w + 'px');
  canvas.setAttribute('height', h + 'px');
  reshape(canvas, w, h);
}

add_benchmark('vtf');
add_benchmark('uniqtex');
add_benchmark('caps');

    </script>
  </head>

  <body onload="start()">

    <table>
      <colgroup span="2" width="610"></colgroup>
      <tr>
        <td colspan="2">
          <div>
            <h3>webgl-bench -- WebGL performance info</h3>
      
            <p style="padding-bottom: 16px;">webgl-bench runs a series of benchmarks and diagnostics
               on your WebGL implementation.  The results are compared
               with a database of results from other users.  Also, by
               default the results are uploaded, to help keep the
               database current and accurate.  We take basic measures
               to anonymize the data.  (You can opt-out of uploading,
               if you prefer not to share).</p>
      
            <div id="init_failure" style="display: none;">
              <span style="background-color: #ffff00">Failed to initialize WebGL &#x2639;</span>
              <p><a href="http://khronos.org/webgl/wiki/Getting_a_WebGL_Implementation">See
                  here for how to get a working WebGL implementation.</a></p>
            </div>
          </div>
        </td>
      </tr>

      <tr>
	<td valign="top">
	  <div class="report_info">
	    <input type="button" id="start_benchmark" value="start benchmark"/>
	    <label><input type="checkbox" id="auto_upload" checked="checked">auto-upload results when finished</label>
	    <input type="button" id="upload_results" value="upload results manually" disabled="1"/>
	    <span id="upload_status"></span><br/>
	    Benchmark Status: <span id="benchmark_status">not started</span><br/>
	    <textarea id="report" style="font-family: monospace; width: 600px; height: 412px;">
	    </textarea>
	  </div>
	</td>
	<td valign="top">
	  <canvas id="canvas" width="640" height="480" style="margin: 3px; width: 600px; height: 450px;"></canvas>
	</td>
      </tr>
      <tr>
        <td colspan="2">
	  <h3>Statistics</h3>
	  <p>These <a href="http://en.wikipedia.org/wiki/Box_plot">box plots</a>
	    show the distribution of uploaded reports for
	    each metric.  The partitions are at the percentiles 1, 25, 50,
	    75 and 99.  The circles are the absolute min and max reported.
	    After you run the benchmarks locally, the green lines show your
	    measured values.
	  </p>
	</td>
      </tr>
      <tr>
	<td valign="top">
          <select id="sb_category_l" onchange="update_plots('statsl', this.value)">
	    <option value="browser">browser</option>
	    <option value="os">os</option>
	    <option value="gpu">gpu</option>
	    <option value="mobile">mobile</option>
	  </select>
	  <div id="statsl" style="background-color: #EEE;">
	  </div>
	</td>
	<td valign="top">
          <select id="sb_category_r" onchange="update_plots('statsr', this.value)">
	    <option value="browser">browser</option>
	    <option value="os">os</option>
	    <option value="gpu">gpu</option>
	    <option value="mobile">mobile</option>
	  </select>
	  <div id="statsr" style="background-color: #EEE;">
	  </div>
	<td>
      </tr>
    </table>
  </body>
</html>
